/*
 * Copyright (c) 2025 DuckDuckGo
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.duckduckgo.duckchat.impl.helper

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.duckchat.impl.ChatState
import com.duckduckgo.duckchat.impl.DuckChatInternal
import com.duckduckgo.duckchat.impl.ReportMetric.USER_DID_CREATE_NEW_CHAT
import com.duckduckgo.duckchat.impl.ReportMetric.USER_DID_OPEN_HISTORY
import com.duckduckgo.duckchat.impl.ReportMetric.USER_DID_SELECT_FIRST_HISTORY_ITEM
import com.duckduckgo.duckchat.impl.ReportMetric.USER_DID_SUBMIT_FIRST_PROMPT
import com.duckduckgo.duckchat.impl.ReportMetric.USER_DID_SUBMIT_PROMPT
import com.duckduckgo.duckchat.impl.ReportMetric.USER_DID_TAP_KEYBOARD_RETURN_KEY
import com.duckduckgo.duckchat.impl.helper.RealDuckChatJSHelper.Companion.DUCK_CHAT_FEATURE_NAME
import com.duckduckgo.duckchat.impl.metric.DuckAiMetricCollector
import com.duckduckgo.duckchat.impl.pixel.DuckChatPixels
import com.duckduckgo.duckchat.impl.store.DuckChatDataStore
import com.duckduckgo.js.messaging.api.JsCallbackData
import kotlinx.coroutines.test.runTest
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
class RealDuckChatJSHelperTest {

    @get:Rule
    var coroutineRule = CoroutineTestRule()

    private val mockDuckChat: DuckChatInternal = mock()
    private val mockDataStore: DuckChatDataStore = mock()
    private val mockDuckChatPixels: DuckChatPixels = mock()
    private val mockDuckAiMetricCollector: DuckAiMetricCollector = mock()

    private val testee = RealDuckChatJSHelper(
        duckChat = mockDuckChat,
        dataStore = mockDataStore,
        duckChatPixels = mockDuckChatPixels,
        duckAiMetricCollector = mockDuckAiMetricCollector,
    )

    @Test
    fun whenMethodIsUnknownThenReturnNull() = runTest {
        val featureName = "aiChat"
        val method = "unknownMethod"
        val id = "123"

        val result = testee.processJsCallbackMessage(featureName, method, id, null)

        assertNull(result)
    }

    @Test
    fun whenGetAIChatNativeHandoffDataAndIdIsNullThenReturnNull() = runTest {
        val featureName = "aiChat"
        val method = "getAIChatNativeHandoffData"

        val result = testee.processJsCallbackMessage(featureName, method, null, null)

        assertNull(result)
    }

    @Test
    fun whenGetAIChatNativeHandoffDataAndDuckChatFeatureEnabledThenReturnJsCallbackDataWithDuckChatEnabled() = runTest {
        val featureName = "aiChat"
        val method = "getAIChatNativeHandoffData"
        val id = "123"

        whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(true)
        whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn("preferences")

        val result = testee.processJsCallbackMessage(featureName, method, id, null)

        val jsonPayload = JSONObject().apply {
            put("platform", "android")
            put("isAIChatHandoffEnabled", true)
            put("aiChatPayload", "preferences")
        }

        val expected = JsCallbackData(jsonPayload, featureName, method, id)

        assertEquals(expected.id, result!!.id)
        assertEquals(expected.method, result.method)
        assertEquals(expected.featureName, result.featureName)
        assertEquals(expected.params.toString(), result.params.toString())
    }

    @Test
    fun whenGetAIChatNativeHandoffDataAndDuckChatFeatureDisabledThenReturnJsCallbackDataWithDuckChatDisabled() = runTest {
        val featureName = "aiChat"
        val method = "getAIChatNativeHandoffData"
        val id = "123"

        whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(false)
        whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn("preferences")

        val result = testee.processJsCallbackMessage(featureName, method, id, null)

        val jsonPayload = JSONObject().apply {
            put("platform", "android")
            put("isAIChatHandoffEnabled", false)
            put("aiChatPayload", "preferences")
        }

        val expected = JsCallbackData(jsonPayload, featureName, method, id)

        assertEquals(expected.id, result!!.id)
        assertEquals(expected.method, result.method)
        assertEquals(expected.featureName, result.featureName)
        assertEquals(expected.params.toString(), result.params.toString())
    }

    @Test
    fun whenGetAIChatNativeHandoffDataAndPreferencesNullThenReturnJsCallbackDataWithPreferencesNull() = runTest {
        val featureName = "aiChat"
        val method = "getAIChatNativeHandoffData"
        val id = "123"

        whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(true)
        whenever(mockDataStore.fetchAndClearUserPreferences()).thenReturn(null)

        val result = testee.processJsCallbackMessage(featureName, method, id, null)

        val jsonPayload = JSONObject().apply {
            put("platform", "android")
            put("isAIChatHandoffEnabled", true)
            put("aiChatPayload", null)
        }

        val expected = JsCallbackData(jsonPayload, featureName, method, id)

        assertEquals(expected.id, result!!.id)
        assertEquals(expected.method, result.method)
        assertEquals(expected.featureName, result.featureName)
        assertEquals(expected.params.toString(), result.params.toString())
    }

    @Test
    fun whenGetAIChatNativeConfigValuesAndIdIsNullThenReturnNull() = runTest {
        val featureName = "aiChat"
        val method = "getAIChatNativeConfigValues"

        val result = testee.processJsCallbackMessage(featureName, method, null, null)

        assertNull(result)
    }

    @Test
    fun whenGetAIChatNativeConfigValuesAndDuckChatFeatureEnabledThenReturnJsCallbackDataWithDuckChatEnabled() = runTest {
        val featureName = "aiChat"
        val method = "getAIChatNativeConfigValues"
        val id = "123"

        whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(true)
        whenever(mockDuckChat.isDuckChatFullScreenModeEnabled()).thenReturn(false)

        val result = testee.processJsCallbackMessage(featureName, method, id, null)

        val jsonPayload = JSONObject().apply {
            put("platform", "android")
            put("isAIChatHandoffEnabled", true)
            put("supportsClosingAIChat", true)
            put("supportsOpeningSettings", true)
            put("supportsNativeChatInput", false)
            put("supportsURLChatIDRestoration", false)
            put("supportsImageUpload", false)
            put("supportsStandaloneMigration", false)
            put("supportsAIChatFullMode", false)
        }

        val expected = JsCallbackData(jsonPayload, featureName, method, id)

        assertEquals(expected.id, result!!.id)
        assertEquals(expected.method, result.method)
        assertEquals(expected.featureName, result.featureName)
        assertEquals(expected.params.toString(), result.params.toString())
    }

    @Test
    fun whenGetAIChatNativeConfigValuesAndDuckChatFeatureDisabledThenReturnJsCallbackDataWithDuckChatDisabled() = runTest {
        val featureName = "aiChat"
        val method = "getAIChatNativeConfigValues"
        val id = "123"

        whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(false)
        whenever(mockDuckChat.isDuckChatFullScreenModeEnabled()).thenReturn(false)

        val result = testee.processJsCallbackMessage(featureName, method, id, null)

        val jsonPayload = JSONObject().apply {
            put("platform", "android")
            put("isAIChatHandoffEnabled", false)
            put("supportsClosingAIChat", true)
            put("supportsOpeningSettings", true)
            put("supportsNativeChatInput", false)
            put("supportsURLChatIDRestoration", false)
            put("supportsImageUpload", false)
            put("supportsStandaloneMigration", false)
            put("supportsAIChatFullMode", false)
        }

        val expected = JsCallbackData(jsonPayload, featureName, method, id)

        assertEquals(expected.id, result!!.id)
        assertEquals(expected.method, result.method)
        assertEquals(expected.featureName, result.featureName)
        assertEquals(expected.params.toString(), result.params.toString())
    }

    @Test
    fun whenGetAIChatNativeConfigValuesAndDuckChatFeatureEnabledAndFullScreenModeEnabledThenReturnJsCallbackDataWithCorrectData() = runTest {
        val featureName = "aiChat"
        val method = "getAIChatNativeConfigValues"
        val id = "123"

        whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(true)
        whenever(mockDuckChat.isDuckChatFullScreenModeEnabled()).thenReturn(true)

        val result = testee.processJsCallbackMessage(featureName, method, id, null)

        val jsonPayload = JSONObject().apply {
            put("platform", "android")
            put("isAIChatHandoffEnabled", true)
            put("supportsClosingAIChat", true)
            put("supportsOpeningSettings", true)
            put("supportsNativeChatInput", false)
            put("supportsURLChatIDRestoration", true)
            put("supportsImageUpload", false)
            put("supportsStandaloneMigration", false)
            put("supportsAIChatFullMode", true)
        }

        val expected = JsCallbackData(jsonPayload, featureName, method, id)

        assertEquals(expected.id, result!!.id)
        assertEquals(expected.method, result.method)
        assertEquals(expected.featureName, result.featureName)
        assertEquals(expected.params.toString(), result.params.toString())
    }

    @Test
    fun whenGetAIChatNativeConfigValuesAnStandaloneMigrationEnabledThenReturnJsCallbackDataWithCorrectData() = runTest {
        val featureName = "aiChat"
        val method = "getAIChatNativeConfigValues"
        val id = "123"

        whenever(mockDuckChat.isStandaloneMigrationEnabled()).thenReturn(true)

        val result = testee.processJsCallbackMessage(featureName, method, id, null)

        val jsonPayload = JSONObject().apply {
            put("platform", "android")
            put("isAIChatHandoffEnabled", false)
            put("supportsClosingAIChat", true)
            put("supportsOpeningSettings", true)
            put("supportsNativeChatInput", false)
            put("supportsURLChatIDRestoration", false)
            put("supportsImageUpload", false)
            put("supportsStandaloneMigration", true)
            put("supportsAIChatFullMode", false)
        }

        val expected = JsCallbackData(jsonPayload, featureName, method, id)

        assertEquals(expected.id, result!!.id)
        assertEquals(expected.method, result.method)
        assertEquals(expected.featureName, result.featureName)
        assertEquals(expected.params.toString(), result.params.toString())
    }

    @Test
    fun whenOpenAIChatAndHasPayloadThenUpdateStoreAndOpenDuckChat() = runTest {
        val featureName = "aiChat"
        val method = "openAIChat"
        val id = "123"
        val payload = JSONObject(mapOf("key" to "value"))
        val payloadString = payload.toString()
        val data = JSONObject(mapOf("aiChatPayload" to payloadString))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDataStore).updateUserPreferences(payloadString)
        verify(mockDuckChat).openNewDuckChatSession()
    }

    @Test
    fun whenOpenAIChatAndDataIsNullThenUpdateStoreAndOpenDuckChat() = runTest {
        val featureName = "aiChat"
        val method = "openAIChat"
        val id = "123"

        assertNull(testee.processJsCallbackMessage(featureName, method, id, null))
        verify(mockDataStore).updateUserPreferences(null)
        verify(mockDuckChat).openNewDuckChatSession()
    }

    @Test
    fun whenOpenAIChatAndPayloadIsNullThenUpdateStoreAndOpenDuckChat() = runTest {
        val featureName = "aiChat"
        val method = "openAIChat"
        val id = "123"
        val data = JSONObject(mapOf("aiChatPayload" to JSONObject.NULL))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))
        verify(mockDataStore).updateUserPreferences(null)
        verify(mockDuckChat).openNewDuckChatSession()
    }

    @Test
    fun whenStartStreamNewPromptResponseStateReceivedThenUpdateChatStateWithStartStreamNewPrompt() = runTest {
        val featureName = "aiChat"
        val method = "responseState"
        val id = "123"
        val data = JSONObject(mapOf("status" to "start_stream:new_prompt"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChat).updateChatState(ChatState.START_STREAM_NEW_PROMPT)
    }

    @Test
    fun whenLoadingResponseStateReceivedThenUpdateChatStateWithLoading() = runTest {
        val featureName = "aiChat"
        val method = "responseState"
        val id = "123"
        val data = JSONObject(mapOf("status" to "loading"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChat).updateChatState(ChatState.LOADING)
    }

    @Test
    fun whenStreamingResponseStateReceivedThenUpdateChatStateWithStreaming() = runTest {
        val featureName = "aiChat"
        val method = "responseState"
        val id = "123"
        val data = JSONObject(mapOf("status" to "streaming"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChat).updateChatState(ChatState.STREAMING)
    }

    @Test
    fun whenErrorResponseStateReceivedThenUpdateChatStateWithError() = runTest {
        val featureName = "aiChat"
        val method = "responseState"
        val id = "123"
        val data = JSONObject(mapOf("status" to "error"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChat).updateChatState(ChatState.ERROR)
    }

    @Test
    fun whenReadyResponseStateReceivedThenUpdateChatStateWithReady() = runTest {
        val featureName = "aiChat"
        val method = "responseState"
        val id = "123"
        val data = JSONObject(mapOf("status" to "ready"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChat).updateChatState(ChatState.READY)
    }

    @Test
    fun whenBlockedResponseStateReceivedThenUpdateChatStateWithBlocked() = runTest {
        val featureName = "aiChat"
        val method = "responseState"
        val id = "123"
        val data = JSONObject(mapOf("status" to "blocked"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChat).updateChatState(ChatState.BLOCKED)
    }

    @Test
    fun whenHideChatInputThenUpdateChatStateWithHide() = runTest {
        val featureName = "aiChat"
        val method = "hideChatInput"
        val id = "123"

        assertNull(testee.processJsCallbackMessage(featureName, method, id, null))

        verify(mockDuckChat).updateChatState(ChatState.HIDE)
    }

    @Test
    fun whenShowChatInputThenUpdateChatStateWithShow() = runTest {
        val featureName = "aiChat"
        val method = "showChatInput"
        val id = "123"

        assertNull(testee.processJsCallbackMessage(featureName, method, id, null))

        verify(mockDuckChat).updateChatState(ChatState.SHOW)
    }

    @Test
    fun whenGetAIChatNativeConfigValuesAndSupportsImageUploadThenReturnJsCallbackDataWithSupportsImageUploadEnabled() = runTest {
        val featureName = "aiChat"
        val method = "getAIChatNativeConfigValues"
        val id = "123"

        whenever(mockDuckChat.isDuckChatFeatureEnabled()).thenReturn(true)
        whenever(mockDuckChat.isImageUploadEnabled()).thenReturn(true)
        whenever(mockDuckChat.isDuckChatFullScreenModeEnabled()).thenReturn(false)

        val result = testee.processJsCallbackMessage(featureName, method, id, null)

        val expectedPayload = JSONObject().apply {
            put("platform", "android")
            put("isAIChatHandoffEnabled", true)
            put("supportsClosingAIChat", true)
            put("supportsOpeningSettings", true)
            put("supportsNativeChatInput", false)
            put("supportsURLChatIDRestoration", false)
            put("supportsImageUpload", true)
            put("supportsStandaloneMigration", false)
            put("supportsAIChatFullMode", false)
        }

        assertEquals(expectedPayload.toString(), result!!.params.toString())
    }

    @Test
    fun whenReportMetricWithoutDataThenPixelNotSent() = runTest {
        val featureName = "aiChat"
        val method = "reportMetric"
        val id = "123"

        assertNull(testee.processJsCallbackMessage(featureName, method, id, null))

        verifyNoInteractions(mockDuckChatPixels)
    }

    @Test
    fun whenReportMetricWithDataThenPixelSentAndCollectMetric() = runTest {
        val featureName = "aiChat"
        val method = "reportMetric"
        val id = "123"
        val data = JSONObject(mapOf("metricName" to "userDidSubmitPrompt"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChatPixels).sendReportMetricPixel(USER_DID_SUBMIT_PROMPT)
        verify(mockDuckAiMetricCollector).onMessageSent()
    }

    @Test
    fun whenReportMetricWithFirstPromptThenPixelSentAndCollectMetric() = runTest {
        val featureName = "aiChat"
        val method = "reportMetric"
        val id = "123"
        val data = JSONObject(mapOf("metricName" to "userDidSubmitFirstPrompt"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChatPixels).sendReportMetricPixel(USER_DID_SUBMIT_FIRST_PROMPT)
        verify(mockDuckAiMetricCollector).onMessageSent()
    }

    @Test
    fun whenReportMetricWithOpenHistoryThenPixelSent() = runTest {
        val featureName = "aiChat"
        val method = "reportMetric"
        val id = "123"
        val data = JSONObject(mapOf("metricName" to "userDidOpenHistory"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChatPixels).sendReportMetricPixel(USER_DID_OPEN_HISTORY)
    }

    @Test
    fun whenReportMetricWithSelectFirstHistoryItemThenPixelSent() = runTest {
        val featureName = "aiChat"
        val method = "reportMetric"
        val id = "123"
        val data = JSONObject(mapOf("metricName" to "userDidSelectFirstHistoryItem"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChatPixels).sendReportMetricPixel(USER_DID_SELECT_FIRST_HISTORY_ITEM)
    }

    @Test
    fun whenReportMetricWithCreateNewChatThenPixelSent() = runTest {
        val featureName = "aiChat"
        val method = "reportMetric"
        val id = "123"
        val data = JSONObject(mapOf("metricName" to "userDidCreateNewChat"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChatPixels).sendReportMetricPixel(USER_DID_CREATE_NEW_CHAT)
    }

    @Test
    fun whenReportMetricWithKeyboardReturnKeyThenPixelSent() = runTest {
        val featureName = "aiChat"
        val method = "reportMetric"
        val id = "123"
        val data = JSONObject(mapOf("metricName" to "userDidTapKeyboardReturnKey"))

        assertNull(testee.processJsCallbackMessage(featureName, method, id, data))

        verify(mockDuckChatPixels).sendReportMetricPixel(USER_DID_TAP_KEYBOARD_RETURN_KEY)
    }

    @Test
    fun whenOpenKeyboardThenResponseSent() = runTest {
        val featureName = "aiChat"
        val method = "openKeyboard"
        val id = "123"
        val data = JSONObject(mapOf("selector" to "user-prompt"))

        val result = testee.processJsCallbackMessage(featureName, method, id, data)

        val expectedPayload = JSONObject().apply {
            put("selector", "document.getElementsByName(''user-prompt'')[0]?.focus();")
            put("success", true)
            put("error", "")
        }

        assertEquals(expectedPayload.toString(), result!!.params.toString())
    }

    @Test
    fun whenNativeActionNewChatRequestedThenSubscriptionDataSent() = runTest {
        val result = testee.onNativeAction(NativeAction.NEW_CHAT)

        assertEquals("submitNewChatAction", result.subscriptionName)
        assertEquals(DUCK_CHAT_FEATURE_NAME, result.featureName)
    }

    @Test
    fun whenNativeActionHistoryRequestedThenSubscriptionDataSent() = runTest {
        val result = testee.onNativeAction(NativeAction.SIDEBAR)

        assertEquals("submitToggleSidebarAction", result.subscriptionName)
        assertEquals(DUCK_CHAT_FEATURE_NAME, result.featureName)
    }

    @Test
    fun whenNativeActionSettingsRequestedThenSubscriptionDataSent() = runTest {
        val result = testee.onNativeAction(NativeAction.DUCK_AI_SETTINGS)

        assertEquals("submitOpenSettingsAction", result.subscriptionName)
        assertEquals(DUCK_CHAT_FEATURE_NAME, result.featureName)
    }
}
